Kotlin 抽象類別、介面與 Object:從規格設計到單例模式
前一天介紹了 Kotlin 的繼承、覆寫與多型。今天要繼續學習物件導向的重要工具:抽象類別、介面、巢狀類別、內部類別、單例物件、伴生物件、匿名物件與函式式介面。這些語法在 Android 的畫面事件、資料管理、Fragment 建立與架構設計中都很常見。
參考資料:Kotlin 類別
假設我們要建立一套繪圖系統,所有圖形都必須計算面積,但不同圖形的計算公式不同:
我們可以先建立一個共同的 Shape 父類別,但父類別本身並不知道面積該如何計算,因此可以將它宣告為抽象類別。
參考資料:Kotlin 抽象類別
abstract class Shape {
abstract fun area(): Double
}
abstract 表示尚未完成、必須由子類別實作。抽象類別不能直接建立物件,因此下面的程式會發生錯誤。
參考資料:Kotlin 抽象類別
// 錯誤:抽象類別不能直接建立物件
// val shape = Shape()
抽象父類別,裡面同時包含抽象屬性、一般屬性、抽象方法與一般方法。
參考資料:Kotlin 抽象類別
abstract class Base {
// 抽象屬性沒有初始值,必須由子類別實作
abstract var a: Int
// 一般屬性可以直接提供初始值
var b: Int = 0
// 抽象方法沒有方法內容
abstract fun fun1()
// 一般方法可以直接提供方法內容
fun fun2() {
println("From Base class")
}
}
抽象類別不代表所有成員都必須是抽象的。它可以同時具有完整實作與尚未實作的成員。
參考資料:Kotlin 抽象類別
普通類別繼承抽象類別後,必須使用 override 完成所有抽象成員。
參考資料:Kotlin 覆寫成員
class Derived : Base() {
// 完成父類別要求的抽象屬性
override var a: Int = 0
// 完成父類別要求的抽象方法
override fun fun1() {
println("From Derived class")
}
}
完整測試程式如下:
參考資料:Kotlin 繼承
fun main() {
val myClass = Derived()
println("a = ${myClass.a}")
println("b = ${myClass.b}")
myClass.fun1()
myClass.fun2()
}
執行結果:
a = 0
b = 0
From Derived class
From Base class
原始範例將函式放進 println():
println(myClass.fun1())
因為 fun1() 沒有回傳值,其回傳型別是 Unit,因此會額外印出 kotlin.Unit。若只想執行方法,直接呼叫 myClass.fun1() 即可。
參考資料:Kotlin Unit
圖形範例,我們可以將原本的普通父類別改為抽象類別,使設計更加明確。
參考資料:Kotlin 抽象類別
import kotlin.math.PI
// 抽象父類別:規定所有圖形都必須提供 area()
abstract class Shape {
// 父類別不知道公式,因此交給子類別完成
abstract fun area(): Double
}
// 圓形繼承 Shape
class Circle(
val radius: Double
) : Shape() {
override fun area(): Double {
return PI * radius * radius
}
}
// 矩形繼承 Shape
class Rectangle(
val width: Double,
val height: Double
) : Shape() {
override fun area(): Double {
return width * height
}
}
fun main() {
// 使用父類別型別接收不同子類別物件
val circle: Shape = Circle(radius = 5.0)
val rectangle: Shape = Rectangle(
width = 10.0,
height = 4.0
)
println(
"圓形面積:${String.format("%.2f", circle.area())}"
)
println(
"矩形面積:${String.format("%.2f", rectangle.area())}"
)
}
執行結果:
圓形面積:78.54
矩形面積:40.00
雖然 circle 與 rectangle 的變數型別都是 Shape,實際執行 area() 時,仍會依照物件的真實類型呼叫對應版本,這就是多型。
參考資料:Kotlin 覆寫方法
介面可以看成一份「類別必須遵守的規格」。介面可以宣告抽象方法、抽象屬性,也可以提供具有方法內容的預設實作。
參考資料:Kotlin 介面
例如,成績單必須具有數學成績、英文成績與顯示成績的方法:
參考資料:Kotlin 介面屬性
interface IntB {
// 介面中的抽象屬性
var math: Int
var eng: Int
// 介面可以提供具有內容的方法
fun show() {
println("math = $math, eng = $eng")
}
}
由於 math 和 eng 沒有初始值,也沒有提供 get() 存取器,因此實作此介面的類別必須完成這兩個屬性。
參考資料:Kotlin 介面屬性
class Report(
override var math: Int = 0,
override var eng: Int = 0
) : IntB
Report 沒有重新實作 show(),所以會直接使用介面提供的預設版本。
參考資料:Kotlin 介面
fun main() {
val report = Report(
math = 95,
eng = 88
)
report.show()
}
執行結果:
math = 95, eng = 88
如果介面中的方法沒有方法內容,實作類別就必須使用 override 完成它。
參考資料:Kotlin 實作介面
interface Drawable {
fun draw()
}
建立 SimpleCircle 實作 Drawable:
參考資料:Kotlin 實作介面
class SimpleCircle : Drawable {
override fun draw() {
println("Drawing a circle")
}
}
測試程式:
fun main() {
val circle = SimpleCircle()
circle.draw()
}
執行結果:
Drawing a circle
Kotlin 的類別只能直接繼承一個父類別,但可以實作多個介面。多個介面之間使用逗號分隔。
參考資料:Kotlin 實作多個介面
interface Drawable {
fun draw()
}
interface Clickable {
fun click()
}
open class Button : Drawable, Clickable {
override fun draw() {
println("Drawing a button")
}
override fun click() {
println("Button clicked")
}
}
測試程式如下:
fun main() {
val button = Button()
button.draw()
button.click()
}
執行結果:
Drawing a button
Button clicked
這種設計可以把不同的能力拆開。例如一個按鈕可以同時具有「繪製」與「點擊」能力,不需要把所有功能都塞進同一個父類別。
參考資料:Kotlin 介面
我們 建立 PaymentMethod 付款方式介面。所有付款方式都必須實作 pay()。
參考資料:Kotlin 介面
interface PaymentMethod {
// amount 是付款金額
// 回傳 Boolean 表示付款是否成功
fun pay(amount: Int): Boolean
}
信用卡與現金雖然付款流程不同,但都可以遵守相同的 PaymentMethod 規格。
參考資料:Kotlin 實作介面
class CreditCard : PaymentMethod {
init {
println("建立 CreditCard 物件")
}
override fun pay(amount: Int): Boolean {
if (amount <= 0) {
println("信用卡付款失敗:金額必須大於 0")
return false
}
println("使用信用卡付款:$amount 元")
return true
}
}
class Cash : PaymentMethod {
init {
println("建立 Cash 物件")
}
override fun pay(amount: Int): Boolean {
if (amount <= 0) {
println("現金付款失敗:金額必須大於 0")
return false
}
println("使用現金付款:$amount 元")
return true
}
}
接著建立 Checkout。它不必知道傳進來的是現金還是信用卡,只需要知道對方遵守 PaymentMethod 介面。
參考資料:Kotlin 介面與多型
class Checkout {
init {
println("建立 Checkout 物件")
}
fun processPayment(
amount: Int,
method: PaymentMethod
) {
val result = method.pay(amount)
if (result) {
println("結帳系統:付款完成")
} else {
println("結帳系統:付款失敗")
}
}
}
完整主程式:
fun main() {
print("請輸入信用卡付款金額:")
val creditAmount = readln().toIntOrNull() ?: 0
print("請輸入現金付款金額:")
val cashAmount = readln().toIntOrNull() ?: 0
println()
println("執行付款流程")
val checkout = Checkout()
val creditCard = CreditCard()
val cash = Cash()
println()
checkout.processPayment(
amount = creditAmount,
method = creditCard
)
println()
checkout.processPayment(
amount = cashAmount,
method = cash
)
}
未來如果加入 LINE Pay、Apple Pay 或悠遊卡,只要建立新的類別實作 PaymentMethod,就不需要修改 Checkout。這正是介面帶來的擴充性。
參考資料:Kotlin 介面
抽象類別與介面都可以制定子類別應遵守的規則,但用途並不完全相同。
參考資料:Kotlin 抽象類別、Kotlin 介面
| 比較項目 | 抽象類別 | 介面 |
|---|---|---|
| 宣告方式 | abstract class |
interface |
| 是否能建立物件 | 不可以 | 不可以 |
| 是否能有建構子 | 可以 | 不可以 |
| 是否能保存物件狀態 | 可以 | 介面本身不能保存狀態 |
| 是否能有一般方法 | 可以 | 可以 |
| 是否能有抽象方法 | 可以 | 可以 |
| 類別可使用數量 | 只能繼承一個父類別 | 可以實作多個介面 |
| 適合用途 | 表達「是什麼」 | 表達「能做什麼」 |
例如:
Circle 是一種 Shape,適合使用抽象類別。Button 能夠被點擊、能夠被繪製,適合使用介面。CreditCard 與 Cash 都能付款,因此共同實作 PaymentMethod。參考資料:Kotlin 類別與介面
Kotlin 可以在類別裡面再宣告類別,這種設計稱為巢狀類別。
參考資料:Kotlin 巢狀與內部類別
class Outer {
val message = "Hello from Outer"
class Nested {
fun greet(): String {
return "Hello from Nested"
}
}
}
建立巢狀類別時,不必先建立外部類別物件,可以直接使用 Outer.Nested()。
參考資料:Kotlin 巢狀類別
fun main() {
val nested = Outer.Nested()
println(nested.greet())
}
巢狀類別不會持有外部類別物件的參考,因此不能直接存取外部類別的 message。
參考資料:Kotlin 巢狀類別
class Outer {
val message = "Hello from Outer"
class Nested {
fun greet(): String {
// 錯誤:Nested 無法直接存取 message
// return message
return "Hello from Nested"
}
}
}
如果內層類別需要存取外部類別的屬性與方法,必須在類別前面加上 inner。
參考資料:Kotlin 內部類別
class OuterIn {
val message = "Hello from OuterIn"
fun outerFun() {
println("This is an OuterIn function")
}
inner class Inner {
// 可以直接存取外部類別的 message
fun greet(): String {
return "Message is $message"
}
// 可以直接呼叫外部類別的方法
fun innerFun() {
outerFun()
}
}
}
建立 Inner 前必須先建立 OuterIn 物件,因為每個 Inner 都會與某個外部物件產生關聯。
參考資料:Kotlin 內部類別
fun main() {
val outerIn = OuterIn()
val inner = outerIn.Inner()
println(inner.greet())
inner.innerFun()
}
執行結果:
Message is Hello from OuterIn
This is an OuterIn function
一般類別可以重複建立多個物件;object 宣告則會建立單一實例,整個程式共用同一個物件,因此常被稱為單例模式。
參考資料:Kotlin Object 宣告
object Logger {
private var count: Int = 0
init {
println("Welcome to Logger")
}
fun log(message: String) {
println("Log:$message")
count++
}
fun logCount(): Int {
return count
}
}
使用 object 時不需要加上小括號建立物件,直接使用物件名稱即可。
參考資料:Kotlin Object 宣告
fun main() {
Logger.log("App 啟動")
Logger.log("使用者完成登入")
println("目前紀錄數量:${Logger.logCount()}")
}
執行結果:
Welcome to Logger
Log:App 啟動
Log:使用者完成登入
目前紀錄數量:2
無論呼叫多少次 Logger,使用的都是同一個物件,所以 count 會持續累加。Kotlin 的具名 object 會在第一次存取時初始化,而且初始化具備執行緒安全性。
參考資料:Kotlin Object 初始化
我們可以建立一個共用的亂數產生器。類別名稱依 Kotlin 慣例調整為大寫開頭的 RandomNumberGenerator。
參考資料:Kotlin 編碼慣例
object RandomNumberGenerator {
var amount: Int = 1
set(value) {
// 數量小於 1 時,自動修正成 1
field = if (value < 1) 1 else value
}
val id: Int = 100
fun generateNumbers(): IntArray {
return IntArray(amount) {
(1..100).random()
}
}
}
完整測試程式:
fun main() {
RandomNumberGenerator.amount = 5
val numbers =
RandomNumberGenerator.generateNumbers()
numbers.forEachIndexed { index, number ->
println("numbers[$index] = $number")
}
println(
"產生器 ID:${RandomNumberGenerator.id}"
)
}
IntArray(amount) { ... } 的 Lambda 會依序建立每一個元素,因此不需要先建立陣列再使用 for 迴圈賦值。
參考資料:Kotlin IntArray
companion object 是宣告在類別內的物件,可以保存常數、共用函式或工廠方法。呼叫時可直接透過類別名稱使用。
參考資料:Kotlin 伴生物件
class RandomNumber {
companion object {
fun getRandomNumber(
amount: Int = 1
): IntArray {
// 【修正】避免傳入 0 或負數
val safeAmount =
if (amount < 1) 1 else amount
return IntArray(safeAmount) {
(1..5).random()
}
}
}
}
呼叫時不需要建立 RandomNumber():
參考資料:Kotlin 伴生物件
fun main() {
val numbers =
RandomNumber.getRandomNumber(5)
numbers.forEachIndexed { index, value ->
println("numbers[$index] = $value")
}
}
雖然伴生物件的呼叫方式看起來像 Java 的 static,但它實際上仍是伴生物件的成員。
參考資料:Kotlin 伴生物件
Android 開發中經常使用伴生物件保存 TAG、請求代碼或固定參數名稱。
參考資料:Kotlin 伴生物件
class User(
val name: String
) {
companion object {
// 編譯階段常數
const val TAG = "User"
// 工廠方法
fun create(name: String): User {
return User(name)
}
}
fun userInfo() {
println("TAG = $TAG, name = $name")
}
}
測試程式:
fun main() {
val user = User.create("Alice")
user.userInfo()
println("類別標籤:${User.TAG}")
}
執行結果:
TAG = User, name = Alice
類別標籤:User
Android 的 Fragment 經常透過伴生物件提供 newInstance() 工廠方法。
參考資料:Android Fragment
下面先使用一般 Kotlin 類別模擬 Fragment:
參考資料:Kotlin 伴生物件
open class Fragment
class DetailFragment : Fragment() {
var param1: String? = null
var param2: String? = null
companion object {
fun newInstance(
param1: String,
param2: String
): DetailFragment {
return DetailFragment().apply {
this.param1 = param1
this.param2 = param2
}
}
}
}
測試程式:
fun main() {
val fragment =
DetailFragment.newInstance(
param1 = "Value 1",
param2 = "Value 2"
)
println("param1 = ${fragment.param1}")
println("param2 = ${fragment.param2}")
}
真正的 Android Fragment 通常應透過 arguments 和 Bundle 傳遞資料,避免 Fragment 被系統重新建立後資料消失。
參考資料:Android Fragment 傳遞參數
class DetailFragment : Fragment() {
companion object {
private const val ARG_PARAM_1 = "param1"
private const val ARG_PARAM_2 = "param2"
fun newInstance(
param1: String,
param2: String
): DetailFragment {
return DetailFragment().apply {
arguments = Bundle().apply {
putString(ARG_PARAM_1, param1)
putString(ARG_PARAM_2, param2)
}
}
}
}
}
如果某個類別或介面的實作只會使用一次,就不一定要另外建立具名類別,可以使用匿名物件。
參考資料:Kotlin 匿名物件
先建立可以被繼承的 Car:
參考資料:Kotlin 繼承
open class Car(
val brand: String
) {
open fun basicInfo(
salesperson: String
): String {
println(
"Car brand = $brand, " +
"salesperson = $salesperson"
)
return "Salesperson is $salesperson"
}
}
使用匿名物件建立一次性的 Car 子類別:
參考資料:Kotlin 匿名物件繼承
fun main() {
val car = object : Car("Toyota") {
override fun basicInfo(
salesperson: String
): String {
println("This car brand is $brand")
return "Salesperson is $salesperson"
}
}
println(
car.basicInfo("Mary")
)
}
object : Car("Toyota") 代表建立一個沒有名稱的子類別,並立即建立它的物件。
參考資料:Kotlin Object Expression
匿名物件也能直接實作介面,這種寫法常用在按鈕監聽器或回呼函式。
參考資料:Kotlin 匿名物件
interface Click {
fun click()
}
使用匿名物件完成 Click:
參考資料:Kotlin 匿名物件
fun main() {
val button = object : Click {
override fun click() {
println("Button clicked")
}
}
button.click()
}
也可以直接把匿名物件當成函式參數:
參考資料:Kotlin Object Expression
fun executeClick(click: Click) {
click.click()
}
fun main() {
executeClick(
object : Click {
override fun click() {
println("匿名物件被點擊")
}
}
)
}
如果介面只有一個抽象方法,可以在 interface 前面加上 fun,成為函式式介面,也稱為 SAM 介面。SAM 是 Single Abstract Method 的縮寫。
參考資料:Kotlin 函式式介面
fun interface Action {
fun execute()
}
建立接收 Action 的函式:
參考資料:Kotlin SAM 轉換
fun runAction(action: Action) {
println("開始執行 runAction")
action.execute()
println("結束執行 runAction")
}
傳統做法是建立匿名物件:
參考資料:Kotlin 匿名物件
fun main() {
runAction(
object : Action {
override fun execute() {
println("使用匿名物件執行")
}
}
)
}
使用函式式介面後,可以透過 Lambda 簡化:
參考資料:Kotlin SAM 轉換
fun main() {
runAction {
println("使用 Lambda 執行")
}
}
執行結果:
開始執行 runAction
使用 Lambda 執行
結束執行 runAction
建立一個 Object_3.kt 有以下寫法:
runAction {
object : Action {
override fun execute() {
println("Executing implementation!")
}
}
}
這段 Lambda 的最後一行只是建立並回傳匿名物件,並沒有呼叫匿名物件的 execute();而 runAction() 需要的 Lambda 本身就是 Action.execute() 的內容,因此應直接寫成:
參考資料:Kotlin SAM 轉換
runAction {
println("Executing implementation!")
}
原始 DoAction 內呼叫了 TODO("Not yet implemented")。TODO() 的作用是直接丟出 NotImplementedError,所以程式執行到這一行就會中止。
參考資料:Kotlin TODO
原始程式:
class DoAction : Action {
override fun execute() {
println("Executing DoAction!")
action()
}
private fun action() {
TODO("Not yet implemented")
}
}
完成後的修正版如下:
參考資料:Kotlin 函式式介面
class DoAction : Action {
override fun execute() {
println("Executing DoAction!")
action()
}
private fun action() {
// 【修正】完成實際功能,避免 TODO() 導致程式中止
println("DoAction 的內部工作已完成")
}
}
完整測試程式:
fun interface Action {
fun execute()
}
fun runAction(action: Action) {
println("開始執行 runAction")
action.execute()
println("結束執行 runAction")
}
class DoAction : Action {
override fun execute() {
println("Executing DoAction!")
action()
}
private fun action() {
// 【修正】不可保留 TODO(),否則執行時會發生例外
println("DoAction 的內部工作已完成")
}
}
fun main() {
// 使用 Lambda 建立 Action
runAction {
println("Executing action 1")
}
println()
// 使用具名類別建立 Action
val myAction = DoAction()
runAction(myAction)
}
建立一父類別型別的變數若要存取子類別特有的 name 屬性,就需要先確認或轉換類型。
參考資料:Kotlin 型別檢查與轉型
val employee: Employee =
FullTimeEmployee(
name = "Mary",
monthlySalary = 50_000.0
)
如果確定物件一定是 FullTimeEmployee,可以使用 as:
參考資料:Kotlin 強制轉型
val fullTimeEmployee =
employee as FullTimeEmployee
println(fullTimeEmployee.name)
如果實際類型不符,as 會產生 ClassCastException。因此不確定類型時,應使用安全轉型 as?。
參考資料:Kotlin 安全轉型
val partTimeEmployee =
employee as? PartTimeEmployee
if (partTimeEmployee == null) {
println("employee 不是 PartTimeEmployee")
}
更推薦使用 is 搭配智慧型轉型:
參考資料:Kotlin 智慧型轉型
when (employee) {
is FullTimeEmployee -> {
// 經過 is 判斷後,可直接存取 name
println(
"${employee.name} 的年薪是 " +
employee.calculateSalary()
)
}
is PartTimeEmployee -> {
println(
"${employee.name} 的薪資是 " +
employee.calculateSalary()
)
}
}
下面整合抽象類別、介面、多型、伴生物件、匿名物件與函式式介面,建立一個可直接執行的付款系統。
參考資料:Kotlin 介面、Kotlin Object
// ==================================================
// 綜合範例
// 抽象類別、介面、單例、伴生物件、匿名物件、SAM
// ==================================================
// 付款結果的抽象父類別
abstract class PaymentResult {
// 每種結果都必須提供訊息
abstract val message: String
// 一般方法可由所有子類別共用
fun printResult() {
println(message)
}
}
// 付款成功
class PaymentSuccess(
private val amount: Int
) : PaymentResult() {
override val message: String
get() = "付款成功,金額:$amount 元"
}
// 付款失敗
class PaymentFailure(
private val reason: String
) : PaymentResult() {
override val message: String
get() = "付款失敗,原因:$reason"
}
// 所有付款方式必須遵守的介面
interface PaymentMethod {
val methodName: String
fun pay(amount: Int): PaymentResult
}
// 信用卡付款
class CreditCard(
private val cardNumber: String
) : PaymentMethod {
override val methodName: String = "信用卡"
override fun pay(amount: Int): PaymentResult {
if (amount <= 0) {
return PaymentFailure(
reason = "付款金額必須大於 0"
)
}
if (cardNumber.length < 4) {
return PaymentFailure(
reason = "信用卡號格式錯誤"
)
}
val lastFour = cardNumber.takeLast(4)
println(
"使用尾號 $lastFour 的信用卡付款"
)
return PaymentSuccess(amount)
}
}
// 現金付款
class Cash : PaymentMethod {
override val methodName: String = "現金"
override fun pay(amount: Int): PaymentResult {
if (amount <= 0) {
return PaymentFailure(
reason = "付款金額必須大於 0"
)
}
println("收取現金:$amount 元")
return PaymentSuccess(amount)
}
}
// 付款紀錄器:整個程式共用同一個物件
object PaymentLogger {
private var paymentCount: Int = 0
fun log(
methodName: String,
result: PaymentResult
) {
paymentCount++
println(
"第 $paymentCount 筆付款:$methodName"
)
result.printResult()
}
fun getPaymentCount(): Int {
return paymentCount
}
}
// 付款完成後的回呼介面
fun interface PaymentCallback {
fun onComplete(result: PaymentResult)
}
// 結帳系統
class Checkout private constructor() {
companion object {
// 使用工廠方法建立 Checkout
fun create(): Checkout {
return Checkout()
}
}
fun processPayment(
amount: Int,
method: PaymentMethod,
callback: PaymentCallback
) {
println("付款方式:${method.methodName}")
val result = method.pay(amount)
PaymentLogger.log(
methodName = method.methodName,
result = result
)
callback.onComplete(result)
}
}
fun main() {
val checkout = Checkout.create()
val creditCard: PaymentMethod =
CreditCard(
cardNumber = "1234567812345678"
)
val cash: PaymentMethod = Cash()
println("=== 第一筆付款 ===")
checkout.processPayment(
amount = 1_500,
method = creditCard
) { result ->
// 使用 is 判斷實際的付款結果
when (result) {
is PaymentSuccess -> {
println("回呼通知:可以準備出貨")
}
is PaymentFailure -> {
println("回呼通知:請重新付款")
}
}
}
println()
println("=== 第二筆付款 ===")
checkout.processPayment(
amount = 500,
method = cash
) { result ->
result.printResult()
}
println()
println("=== 第三筆付款:匿名付款方式 ===")
// 建立只使用一次的匿名付款方式
val couponPayment =
object : PaymentMethod {
override val methodName: String =
"購物金"
override fun pay(
amount: Int
): PaymentResult {
return if (amount in 1..1_000) {
PaymentSuccess(amount)
} else {
PaymentFailure(
reason = "購物金最高只能折抵 1,000 元"
)
}
}
}
checkout.processPayment(
amount = 800,
method = couponPayment
) { result ->
result.printResult()
}
println()
println(
"付款總筆數:" +
PaymentLogger.getPaymentCount()
)
}
今天的語法並不是單純的 Kotlin 練習,在 Android 專案中都有對應用途。
參考資料:Android Developers
| Kotlin 功能 | Android 常見用途 |
|---|---|
| 抽象類別 | 共用的 BaseActivity、BaseFragment |
| 介面 | 點擊事件、資料回呼、Repository 規格 |
| 多介面實作 | 同時支援點擊、拖曳、生命週期事件 |
| 巢狀類別 | 將只屬於某個類別的型別放在類別內 |
| 內部類別 | 存取 Activity 或 Adapter 外部成員 |
object |
Logger、資料管理器、共用設定 |
companion object |
TAG、常數、newInstance() |
| 匿名物件 | OnClickListener、Callback |
fun interface |
使用 Lambda 簡化單一方法介面 |
is/as? |
判斷 View、Fragment 或資料型別 |
Android 傳統 View 系統中的點擊監聽器就是常見例子:
參考資料:Android View.OnClickListener
button.setOnClickListener(
object : View.OnClickListener {
override fun onClick(view: View?) {
println("按鈕被點擊")
}
}
)
因為 OnClickListener 只有一個抽象方法,所以可簡化成 Lambda:
參考資料:Android 點擊事件
button.setOnClickListener {
println("按鈕被點擊")
}
今天學習的內容如下:
參考資料:Kotlin 官方文件
abstract class 用來建立不能直接實例化的抽象父類別。interface 用來定義類別必須遵守的規格。inner class 可以直接存取外部類別的成員。object 可建立整個程式共用的單例物件。companion object 可提供類別層級的常數與工廠方法。object : Type 可以建立一次性使用的匿名物件。fun interface 可以透過 SAM 轉換使用 Lambda。is 可搭配智慧型轉型安全判斷物件類型。as 轉型失敗會發生例外,不確定時應使用 as?。參考資料:Kotlin 介面、Kotlin Object
請設計一套通知系統,包含以下內容:
參考資料:Kotlin 介面
interface Notification {
fun send(message: String): Boolean
}
請建立三個實作類別:
EmailNotification
SmsNotification
AppNotification
每一個類別都必須覆寫 send(),輸出不同的傳送方式。
參考資料:Kotlin 實作介面
再建立一個 NotificationManager 單例物件:
object NotificationManager {
fun notify(
message: String,
notification: Notification
) {
val result =
notification.send(message)
println(
if (result) {
"通知傳送成功"
} else {
"通知傳送失敗"
}
)
}
}
最後分別傳入 Email、簡訊與 App 通知物件,觀察同一個 notify() 如何透過介面多型執行不同功能。
參考資料:Kotlin Object 宣告
抽象類別負責建立共同基礎,介面負責規定物件具備哪些能力,object 負責提供共用的單一實例,而匿名物件與函式式介面則能簡化只使用一次的事件與回呼。掌握這些觀念後,就能寫出更容易擴充、測試與維護的 Kotlin 程式。
參考資料:Kotlin 官方文件
下一篇可以接續介紹 Kotlin 的資料類別 data class、列舉類別 enum class 與密封類別 sealed class,學習如何建立更安全、更容易管理的 App 資料模型。
參考資料:Kotlin Data Classes、Kotlin Enum Classes、Kotlin Sealed Classes